{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6c6b84a0",
   "metadata": {
    "editable": true,
    "id": "0l5n9ToXGbRC",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "# Week 4: Multi-class Classification\n",
    "\n",
    "Welcome to the last assignment! In this notebook, you will get a chance to work on a multi-class classification problem. You will be using the [Sign Language MNIST](https://www.kaggle.com/datasets/datamunge/sign-language-mnist) dataset, which contains 28x28 images of hands depicting the letters of the english alphabet. \n",
    "\n",
    "#### TIPS FOR SUCCESSFUL GRADING OF YOUR ASSIGNMENT:\n",
    "\n",
    "- All cells are frozen except for the ones where you need to submit your solutions or when explicitly mentioned you can interact with it.\n",
    "\n",
    "\n",
    "- You can add new cells to experiment but these will be omitted by the grader, so don't rely on newly created cells to host your solution code, use the provided places for this.\n",
    "\n",
    "- You can add the comment # grade-up-to-here in any graded cell to signal the grader that it must only evaluate up to that point. This is helpful if you want to check if you are on the right track even if you are not done with the whole assignment. Be sure to remember to delete the comment afterwards!\n",
    "\n",
    "- Avoid using global variables unless you absolutely have to. The grader tests your code in an isolated environment without running all cells from the top. As a result, global variables may be unavailable when scoring your submission. Global variables that are meant to be used will be defined in UPPERCASE.\n",
    "\n",
    "- To submit your notebook, save it and then click on the blue submit button at the beginning of the page.\n",
    "\n",
    "Let's get started!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eea99af0",
   "metadata": {
    "deletable": false,
    "editable": false,
    "id": "wYtuKeK0dImp",
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import numpy as np\n",
    "import tensorflow as tf\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5b16fdf8",
   "metadata": {
    "deletable": false,
    "editable": false,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "import unittests"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "378bb0bd",
   "metadata": {
    "editable": true,
    "id": "tnVJZI96IMh0",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "In this assignment you will actually be working with a modified version of the original [Sign Language MNIST](https://www.kaggle.com/datasets/datamunge/sign-language-mnist)  dataset. The original dataset is presented as a csv file, however this makes the pre processing of the data very different from what you have been doing so far. To make loading the images and creating the datasetss more aligned with what you have learned so far, we have already downloaded each image as a .png file. You can find them in the `data/train` and `data/validation` folders. As the names suggest, the images in the first folder will be used for training, and the ones in the latter will be used for validation. \n",
    "\n",
    "Begin by defining some globals with the paths to the training and test folders."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "780e7599",
   "metadata": {
    "deletable": false,
    "editable": false,
    "id": "N3fQlI99JPw9",
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "TRAIN_DIR = 'data/train/'\n",
    "VALIDATION_DIR = 'data/validation/'"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fba4ea85",
   "metadata": {
    "editable": true,
    "id": "iybvOtoHISHV",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "Let's explore the `./data` folder containing the images. There is a subdirectory for each class. In this case there will be 24 folders one for each letter in the alphabet, except for letters J and Z. Because of gesture motions these two letters can't be represented by an image, and are thus not included on the dataset.\n",
    "\n",
    "The complete tree looks like this:\n",
    "\n",
    "```\n",
    ".└── data/\n",
    "    ├── train/\n",
    "    |    ├── A/\n",
    "    |    │   ├── a1.jpg\n",
    "    |    │   ├── a2.jpg\n",
    "    |    │   └── ...\n",
    "    |    ├── B/\n",
    "    |        ├── b1.jpg\n",
    "    |        ├── b2.jpg\n",
    "    |        └── ...\n",
    "    |    ├── ...\n",
    "    |    ├── I/\n",
    "    |    |    ├── i1.jpg\n",
    "    |    |    ├── i2.jpg\n",
    "    |    |    └── ...\n",
    "    |    ├── K/\n",
    "    |    |    ├── k1.jpg\n",
    "    |    |    ├── k2.jpg\n",
    "    |    |    └── ...\n",
    "    |    ├── ...\n",
    "    |    └── Y/\n",
    "    |        ├── y1.jpg\n",
    "    |        ├── y2.jpg\n",
    "    |        └── ...\n",
    "    └── validation/\n",
    "         ├── A/\n",
    "         │   ├── a1.jpg\n",
    "         │   ├── a2.jpg\n",
    "         │   └── ...\n",
    "         ├── B/\n",
    "         |   ├── b1.jpg\n",
    "         |   ├── b2.jpg\n",
    "         |   └── ...\n",
    "         ├── ...\n",
    "         ├── I/\n",
    "         |    ├── i1.jpg\n",
    "         |    ├── i2.jpg\n",
    "         |    └── ...\n",
    "         ├── K/\n",
    "         |    ├── k1.jpg\n",
    "         |    ├── k2.jpg\n",
    "         |    └── ...\n",
    "         ├── ...\n",
    "         └── Y/\n",
    "             ├── y1.jpg\n",
    "             ├── y2.jpg\n",
    "             └── ...\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2c4ec28c-2e17-4fba-bfed-4d70b26c22ab",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "Let's take a look at what the images look like."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "68984793",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "deletable": false,
    "editable": false,
    "id": "ftfeq3JYagks",
    "outputId": "3ed1dd14-7483-425c-d172-b982aeca549c",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(1, 6, figsize=(14, 3))\n",
    "fig.suptitle('Sign Language MNIST Images', fontsize=16)\n",
    "\n",
    "# Plot one image from the first 4 letters\n",
    "for ii, letter in enumerate(['A' , 'B', 'C', 'D', 'E', 'F']):\n",
    "    dir = f'./data/train/{letter}'\n",
    "    img = tf.keras.preprocessing.image.load_img(dir+'/'+os.listdir(dir)[0])\n",
    "    axes[ii].imshow(img)\n",
    "    axes[ii].set_title(f'Example of letter {letter}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34382227-3325-4172-8892-04f739bf5365",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "By plotting the images with `matplotlib` you can readily see images have a resolution of 28x28 (look at the image axes) and are in greyscale, but you can double check this by using the code below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "683dce57-81aa-4217-b10e-6b725137fa1e",
   "metadata": {
    "deletable": false,
    "editable": false,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Convert the image into its numpy array representation\n",
    "sample_array = tf.keras.preprocessing.image.img_to_array(img)\n",
    "\n",
    "print(f\"Each image has shape: {sample_array.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "138dce69-f614-4783-8137-24380aebaa7c",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "Don't worry about the last dimension. That is because the [`img_to_array`](https://www.tensorflow.org/api_docs/python/tf/keras/utils/img_to_array) function returns a 3D array. You can easily check that actually it has repeated the same values in each dimension, for example, take a look at the first 5 columns of the image. All you really care about is that your image is 28x28 pixels. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3436a798-0c8a-41a3-9209-2981b1c7f86a",
   "metadata": {
    "editable": false,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "sample_array[0,:5]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f716d86",
   "metadata": {
    "editable": true,
    "id": "0QNkjIRCN5Kg",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "## Creating the datasets for the CNN\n",
    "\n",
    "### Exercise 1: train_val_datasets\n",
    "\n",
    "Your first task is to code the function that will create the datasets that will yield batches of images, both for training and validation. For this complete the `train_val_generators` function below.\n",
    "\n",
    "For grading purposes, make sure to use a **batch size of 32**. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cc6b5845",
   "metadata": {
    "cellView": "code",
    "deletable": false,
    "editable": true,
    "id": "9ZDIvKGtNISO",
    "lines_to_next_cell": 2,
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED FUNCTION: train_val_datasets\n",
    "def train_val_datasets():\n",
    "    \"\"\"Create train and validation datasets\n",
    "\n",
    "    Returns:\n",
    "        (tf.data.Dataset, tf.data.Dataset): train and validation datasets\n",
    "    \"\"\"\n",
    "    ### START CODE HERE ###\n",
    "    train_dataset = tf.keras.utils.image_dataset_from_directory( \n",
    "        directory=None,\n",
    "        batch_size=None,\n",
    "        image_size=None,\n",
    "        color_mode = 'grayscale', # Use this argument to get just one color dimension, because it is greyscale \n",
    "    ) \n",
    "    \n",
    "    validation_dataset = tf.keras.utils.image_dataset_from_directory( \n",
    "        directory=None,\n",
    "        batch_size=None,\n",
    "        image_size=None,\n",
    "        color_mode = 'grayscale', # Use this argument to get just one color dimension, because it imgs are greyscale \n",
    "    ) \n",
    "    ### END CODE HERE ###\n",
    "    \n",
    "    return train_dataset, validation_dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aea10e2d",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "deletable": false,
    "editable": false,
    "id": "2oxxBZDwPozr",
    "outputId": "5f1a552b-652d-42f1-e3d1-9aa58c7423e1",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Create train and validation datasets\n",
    "train_dataset, validation_dataset = train_val_datasets()\n",
    "print(f\"Images of train dataset have shape: {train_dataset.element_spec[0].shape}\")\n",
    "print(f\"Labels of train dataset have shape: {train_dataset.element_spec[1].shape}\")\n",
    "print(f\"Images of validation dataset have shape: {validation_dataset.element_spec[0].shape}\")\n",
    "print(f\"Labels of validation dataset have shape: {validation_dataset.element_spec[1].shape}\")   "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "01c172cc",
   "metadata": {
    "editable": true,
    "id": "cok5oQa5Rknv",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "**Expected Output:**\n",
    "```\n",
    "Found 27455 files belonging to 24 classes.\n",
    "Found 7173 files belonging to 24 classes.\n",
    "Images of train generator have shape: (None, 28, 28)\n",
    "Labels of train generator have shape: (None)\n",
    "Images of validation generator have shape: (None, 28, 28, 1)\n",
    "Labels of validation generator have shape: (None)\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "701be9fc",
   "metadata": {
    "deletable": false,
    "editable": false,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Test your function\n",
    "unittests.test_train_val_datasets(train_val_datasets)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce27c239",
   "metadata": {
    "editable": true,
    "id": "CkHUj4PsP_jT",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "## Coding the CNN\n",
    "\n",
    "### Exercise : create_model\n",
    "\n",
    "One last step before training is to define the architecture of the model that will be trained.\n",
    "\n",
    "Complete the `create_model` function below. This function should return a Keras' model that uses the [`Sequential`](https://www.tensorflow.org/api_docs/python/tf/keras/Sequential) API.\n",
    "\n",
    "A couple of things to keep in mind when defining the architecture:\n",
    "\n",
    "- Start the model with an [`Input`](https://www.tensorflow.org/api_docs/python/tf/keras/Input) followed by a layer that rescales your images so that each pixel has values between 0 and 1 \n",
    "  \n",
    "- There different ways to implement the output layer, however, we expect the last layer of your model to have a number of units that corresponds to the number of possible categories, as well as the correct activation function.\n",
    "\n",
    "- Aside from defining the architecture of the model, you should also compile it so make sure to use a `loss` function that is suitable for multi-class classification. Remember to also define suitable `metric` to monitor.\n",
    "\n",
    "**Note that you should use no more than 2 Conv2D and 2 MaxPooling2D layers to achieve the desired performance. You can also add dropout layers to improve training**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "db7e081e",
   "metadata": {
    "cellView": "code",
    "deletable": false,
    "editable": true,
    "id": "Rmb7S32cgRqS",
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED FUNCTION: create_model\n",
    "def create_model():\n",
    "    \"\"\"Create the classifier model\n",
    "\n",
    "    Returns:\n",
    "        tf.keras.model.Sequential: CNN for multi-class classification\n",
    "    \"\"\"\n",
    "    ### START CODE HERE ###      \n",
    "    \n",
    "    # Define the model\n",
    "    # Use no more than 2 Conv2D and 2 MaxPooling2D\n",
    "    model = tf.keras.models.Sequential([ \n",
    "        # Define an input layer\n",
    "        tf.keras.layers.InputLayer(None), # Set correct input size\n",
    "        # Rescale images\n",
    "        tf.keras.layers.None(None),\n",
    "        ]) \n",
    "\n",
    "    model.compile(optimizer = None,\n",
    "                  loss = None,\n",
    "                  metrics = [None])\n",
    "\n",
    "    ### END CODE HERE ### \n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65cf159d",
   "metadata": {},
   "source": [
    "The next cell allows you to check the number of total and trainable parameters of your model and prompts a warning in case these exceeds those of a reference solution, this serves the following 3 purposes listed in order of priority:\n",
    "\n",
    "- Helps you prevent crashing the kernel during training.\n",
    "\n",
    "- Helps you avoid longer-than-necessary training times.\n",
    "- Provides a reasonable estimate of the size of your model. In general you will usually prefer smaller models given that they accomplish their goal successfully.\n",
    "\n",
    "\n",
    "**Notice that this is just informative** and may be very well below the actual limit for size of the model necessary to crash the kernel. So even if you exceed this reference you are probably fine. However, **if the kernel crashes during training or it is taking a very long time and your model is larger than the reference, come back here and try to get the number of parameters closer to the reference.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ded3d9df-3490-438f-ba4f-c03500d5ed69",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "deletable": false,
    "editable": false,
    "id": "-e7ywtgZS5pU",
    "lines_to_next_cell": 2,
    "outputId": "5bc05d6f-91fe-4cbf-a6b4-1d7495b47bef",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Create your model\n",
    "model = create_model()\n",
    "\n",
    "# Check parameter count against a reference solution\n",
    "unittests.parameter_count(model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a78e2044-003d-4c4d-b4a7-a81b337fb2a0",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "Check that the input and output shape of your model are correct"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1bf5da08-ec40-468b-bde7-b1f0508bf37f",
   "metadata": {
    "deletable": false,
    "editable": false,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "print(f'Input shape: {model.input_shape}')\n",
    "print(f'Output shape: {model.output_shape}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71fc07ca",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "Expected output:\n",
    "\n",
    "```\n",
    "Input shape: (None, 28, 28, 1)\n",
    "Output shape: (None, 26)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c6c37dc5-d9e1-4205-9d08-aa75ef0d5f84",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "Using the `summary` method you can visulize the model you just defined."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f3a36bbb-efe0-4ab1-bb46-6c183a936265",
   "metadata": {
    "deletable": false,
    "editable": false,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "model.summary()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "42024d2f",
   "metadata": {},
   "source": [
    "\n",
    "Check that the architecture you used is compatible with the dataset (you can ignore the warnings prompted by using the GPU):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c8f5b744",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "for images, labels in train_dataset.take(1):\n",
    "\texample_batch_images = images\n",
    "\texample_batch_labels = labels\n",
    "\t\n",
    "try:\n",
    "\tmodel.evaluate(example_batch_images, example_batch_labels, verbose=False)\n",
    "except:\n",
    "\tprint(\"Your model is not compatible with the dataset you defined earlier. Check that the loss function, last layer and label_mode are compatible with one another.\")\n",
    "else:\n",
    "\tpredictions = model.predict(example_batch_images, verbose=False)\n",
    "\tprint(f\"predictions have shape: {predictions.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "255fe3ea",
   "metadata": {},
   "source": [
    "**Expected output:**\n",
    "\n",
    "```\n",
    "predictions have shape: (32, 26)\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f26fb111",
   "metadata": {
    "deletable": false,
    "editable": false,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Test your function\n",
    "unittests.test_create_model(create_model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8002895d-c41c-41ce-9062-00dd155930a2",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "Finally, you can go ahead and train your model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e11ebaca-f418-452a-b420-e1260fcd89ee",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "deletable": false,
    "editable": false,
    "id": "-e7ywtgZS5pU",
    "lines_to_next_cell": 2,
    "outputId": "5bc05d6f-91fe-4cbf-a6b4-1d7495b47bef",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Train your model\n",
    "history = model.fit(train_dataset,\n",
    "                    epochs=15,\n",
    "                    validation_data=validation_dataset)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed0b45e5-f0ad-4f7d-b6a1-5d4ec6a190b3",
   "metadata": {
    "editable": true,
    "id": "mmpadXR_WGbK",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "Now take a look at your training history:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4abe3c9b",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 545
    },
    "deletable": false,
    "editable": false,
    "id": "_Q3Zpr46dsij",
    "outputId": "906d642c-da0f-4e8e-93af-77e38570fa9b",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Get training and validation accuracies\n",
    "acc = history.history['accuracy']\n",
    "val_acc = history.history['val_accuracy']\n",
    "loss = history.history['loss']\n",
    "val_loss = history.history['val_loss']\n",
    "\n",
    "# Get number of epochs\n",
    "epochs = range(len(acc))\n",
    "\n",
    "fig, ax = plt.subplots(1, 2, figsize=(10, 5))\n",
    "fig.suptitle('Training and validation accuracy')\n",
    "\n",
    "for i, (data, label) in enumerate(zip([(acc, val_acc), (loss, val_loss)], [\"Accuracy\", \"Loss\"])):\n",
    "    ax[i].plot(epochs, data[0], 'r', label=\"Training \" + label)\n",
    "    ax[i].plot(epochs, data[1], 'b', label=\"Validation \" + label)\n",
    "    ax[i].legend()\n",
    "    ax[i].set_xlabel('epochs')\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a35d88f4",
   "metadata": {
    "editable": true,
    "id": "PdWizvXnXAGz",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "You will not be graded based on the accuracy of your model but try making it as high as possible for both training and validation, as an optional exercise, **after submitting your notebook for grading**.\n",
    "\n",
    "A reasonable benchmark is to achieve over 99% accuracy for training and over 95% accuracy for validation within 15 epochs. Try tweaking your model's architecture or the augmentation techniques to see if you can achieve these levels of accuracy."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "85e24649",
   "metadata": {
    "editable": true,
    "id": "DmHC02GaTuDg",
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "**Congratulations on finishing this week's assignment!**\n",
    "\n",
    "You have successfully implemented a convolutional neural network that is able to perform multi-class classification tasks! Nice job!\n",
    "\n",
    "**Keep it up!**"
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "grader_version": "1",
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
